"""Copyright (c) 2009, cr @ netgarage.org
All rights reserved.

Redistribution and use in source and binary forms, with or without modification, 
are permitted provided that the following conditions are met:

    * Redistributions of source code must retain the above copyright notice, 
        this list of conditions and the following disclaimer.
    * Redistributions in binary form must reproduce the above copyright notice, 
        this list of conditions and the following disclaimer in the documentation 
        and/or other materials provided with the distribution.
    * Neither the name of the netgarage nor the names of its contributors
        may be used to endorse or promote products derived from this software 
        without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

For more Information see http://netgarage.org
"""


from __future__ import with_statement
from optparse import OptionParser
import sys
import os
import signal
import ConfigParser
from datetime import datetime, time, timedelta
import time
from subprocess import call
import smtplib
from email.MIMEMultipart import MIMEMultipart
from email.MIMEBase import MIMEBase
from email.MIMEText import MIMEText
from email.Utils import formatdate
from email import Encoders
import logging
import logging.handlers
from Queue import Queue
from threading import Thread

LEVELS = {'debug': logging.DEBUG,
          'info': logging.INFO,
          'warning': logging.WARNING,
          'error': logging.ERROR,
          'critical': logging.CRITICAL}


class NGrbaError(Exception):
    """ Base class for exceptions """
    pass
  
class RsyncError(NGrbaError):
    """exception raised when rsync errors occured"""
    def __init__(self, ba, ec, l):
        self.backup = ba
        self.errorcode = ec
        self.logfile = l
        
    def __str__(self):
        return "Backup: %s exited with rsync error code %d.\n" % (self.backup, self.errorcode)    

class Backup:
    """ class which represents a backup"""
    def __init__(self, n, s, d, o, t, l):
        self.name = n
        self.src = s
        self.dst = d
        self.options = o           
        self.min = t[0]
        self.hour = t[1]
        self.day = t[2]
        self.month = t[3]
        self.dow = t[4]
        self.logdir = l
        self.logfile = None
        
    def __update_logname(self):
        self.logfile = self.logdir + self.name + datetime.now().strftime("_%Y_%b_%d_%I_%M.log")
    
    def run_backup(self):
        """starts the backup processes"""
        self.__update_logname()
        with open(self.logfile, "w", 0) as log: 
            try:
                log.write("[ " + self.name + " ]\n")
                log.write("starting backup.\n\n")
                sys.stdout.flush()
                args = "rsync " + self.options + " " + self.src + " " + self.dst
                ret = call(args.split(), stdout=log, stderr=log) 
                log.write("\n\nbackup finished.\n")
                log.write("backup status: %s\n\n\n" % ret)
           
                if ret > 0:
                    raise RsyncError(self.name, ret, self.logfile) 
            except:
                raise  
        return ("backup %s successfull." % self.name, self.logfile) 

        
    def match(self, t):
        """checks if a backup should be executed at a given time t"""
        return (
                (str(t.minute) == self.min or self.min == "*") and
                (str(t.hour) == self.hour or self.hour == "*") and
                (str(t.month) == self.month or self.month == "*") and
                (str(t.weekday()+1) == self.dow or self.dow == "*")
                )   
        

class NGrba:
    def __init__(self, configfile):
        self.backupqueue = Queue()
        self.configfile = configfile
        self.defaults = { "log_directory":"log/",
                          "pidfile":"/tmp/ngrba.pid",
                          "log_file":"log/ngrba.log",
                          "log_level":"info",
                          "rsync_options":"",
                          "mail_notification":1,
                          }
        self.backups = []   
        
        parser = ConfigParser.SafeConfigParser(self.defaults)
        if not os.path.exists(self.configfile):
            print "can not read config file. %s" % self.configfile
            sys.exit(0)
        parser.read(self.configfile) 
        
        #global config options which have default values
        self.logdir = parser.get("ngrba", "log_directory")
        self.logfile = parser.get("ngrba", "log_file")
        self.loglevel = LEVELS.get(parser.get("ngrba", "log_level"), logging.NOTSET)
        self.pidfile = parser.get("ngrba", "pidfile")
        self.rsync_options = parser.get("ngrba", "rsync_options")
        
        if not os.path.isdir(self.logdir):
            try:
                os.mkdir(self.logdir) 
            except OSError, e:
                print "can not create log directory."
                print e
                sys.exit(0)
            
        #set up the logging 
        self.logger = logging.getLogger("ngrba_log")
        self.logger.setLevel(self.loglevel)
        self.handler = logging.handlers.RotatingFileHandler(self.logfile, maxBytes=1000000, backupCount=10)
        self.formatter = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s")
        self.handler.setFormatter(self.formatter)
        self.logger.addHandler(self.handler)
                           
        try:
            #mail notification
            self.mail_notification = parser.getint("ngrba", "mail_notification")
            if self.mail_notification > 0:
                #neccesary 
                mail_server = parser.get("ngrba", "mail_server")
                mail_user = parser.get("ngrba", "mail_user")
                mail_password = parser.get("ngrba", "mail_password")
                mail_from = parser.get("ngrba", "mail_from_address")
                mail_to = parser.get("ngrba", "mail_to_address")
                self.mailer = MailNotification(mail_server, mail_user, mail_password, mail_from, mail_to)
                self.mailer.logger = self.logger
                #optional
                if parser.has_option("ngrba", "mail_port"):
                    self.mailer.port = int(parser.get("ngrba", "mail_port"))
                if parser.has_option("ngrba", "mail_use_tls"):
                    self.mailer.tls = int(parser.get("ngrba", "mail_use_tls"))
                    
            #backups
            for section in parser.sections():
                if section != "ngrba":
                    backupname = section
                    source = parser.get(section, "source")
                    destination = parser.get(section, "destination")
                    rsync_options = parser.get(section, "rsync_options") + " " + self.rsync_options
                    time = parser.get(section, "time").split()
                    ba = Backup(backupname, source, destination, rsync_options, time, self.logdir)
                    self.backups.append(ba)
                
        except:
            self.logger.exception("error parsing the configuration file.")   
        
        self.running = True     
        
        
    def run_backup(self, backups = []): 
        self.logger.debug("running backups.")
        if not backups:
            for ba in self.backups:
                self.backupqueue.put(ba)
        else:
            for ba in self.backups:
                if ba.name in backups:
                    self.backupqueue.put(ba)
        self.backupqueue.join()
        self.running = False
                
                      
    def backup_thread(self):
        while self.running:
            try:
                backup = self.backupqueue.get()
                if backup is not None:
                    self.logger.info("starting backup: %s" % backup.name)
                    msg,log = backup.run_backup()
                    self.backupqueue.task_done()
                    if self.mail_notification == 2:
                        self.mailer.notify(msg, files = [log])
            #changes to RsyncError as re in 2.6
            except RsyncError, re:
                msg = str(re)
                log = re.logfile
                self.logger.error("error with backup: \n%s." % msg) 
                if self.mail_notification:
                    self.mailer.notify(msg, files = [log])  
            except:
                self.logger.exception("backup error.")
        self.backupqueue.join()

    def start_backup_thread(self):
        #start backup thread
        self.bt = Thread(target = self.backup_thread)
        self.bt.start()
        
        
    def run(self):
        """run function of the daemon"""
        self.start_backup_thread()
        t = datetime(*datetime.now().timetuple()[0:5])
        while self.running:
            for ba in self.backups:
                if ba.match(t):
                    self.logger.debug("%s put in backup queue" % ba.name)
                    self.backupqueue.put(ba)
                                                    
            t += timedelta(minutes=1)
            s = (t - datetime.now()).seconds
            time.sleep(s)
    
    def start_daemon(self):
        """starts the daemon process"""
        if os.path.isfile(self.pidfile):
            print "pid file \"%s\" already exists. daemon already running?" % self.pidfile
            self.running = False
            return
        try:
            pid = os.fork()
            if pid > 0:
                sys.exit(0)
            os.chdir("/")
            os.setsid()
            os.umask(0)
            pid = os.fork()
            if pid > 0:
                sys.exit(0)
                
            os.open(self.logfile, os.O_RDWR)
            os.dup2(0, 1)
            os.dup2(0, 2)
            
            pid = str(os.getpid())
            file(self.pidfile, "w+").write("%s\n" % pid)
        except OSError:
            self.logger.exception("error starting the daemon.") 
        self.logger.info("daemon started.")
        self.run()

        
    def stop_daemon(self):
        """ stops the daemon"""
        if not os.path.isfile(self.pidfile):
            print "pid file \"%s\" does not exist. daemon not running?" % self.pidfile
            return
        try:
            pf = file(self.pidfile, "r")
            pid = int(pf.read().strip())
            pf.close()
            os.kill(pid, signal.SIGTERM)
            os.remove(self.pidfile)
            time.sleep(1)
        except:
            self.logger.exception("error stopping the daemon.") 
        self.logger.info("daemon stopped.")
      
    
class MailNotification():
    def __init__(self, server, user, password, mail_from, mail_to, tls = False, port = 25):
        self.server = server
        self.port = port
        self.user = user
        self.password = password
        self.subject = "ngrba " + os.uname()[1] + " Backup Error " + datetime.now().strftime("%d.%b.%Y")
        self.mail_from = mail_from
        self.mail_to = mail_to
        self.tls = tls
        self.logger = None
        
        
    def notify(self, text, subject = "", mail_from = "", mail_to = "", files = []):
        try:
            """ sends out the email notification """
            s = smtplib.SMTP(self.server, self.port)
            if self.tls:
                s.ehlo()
                s.starttls()
                s.ehlo()
            s.login(self.user, self.password)
            if subject == "":
                subject = self.subject
            if mail_from == "":
                mail_from = self.mail_from
            if mail_to == "":
                mail_to = self.mail_to
            message = self.create_message(mail_from, mail_to, subject, text, files)
            s.sendmail(mail_from, mail_to.split(","), message)
            self.logger.info("mail notification send.")
            s.quit()
        except:
            self.logger.exception("error sending the mail notification") 
                    
    def create_message(self, mail_from, mail_to, subject, text, files):
        msg = MIMEMultipart()
        msg["From"] = mail_from
        msg["To"] = mail_to
        msg["Date"] = formatdate(localtime=True)
        msg["Subject"] = self.subject
        
        msg.attach(MIMEText(text))
        
        for f in files:
            part = MIMEBase("application", "octet-stream")
            part.set_payload(open(f, "rb").read())
            Encoders.encode_base64(part)
            part.add_header("Content-Disposition", "attachment; filename=\"%s\"" % os.path.basename(f))
            msg.attach(part)
        
        return msg.as_string()
            

  
if __name__ == "__main__":
    args_parser = OptionParser()
    args_parser.add_option("-c", "--config", dest="configfile", help="configuration file")
    args_parser.add_option("-d", "--daemon", dest="daemon", help="start/stop/reload the daemon mode")
    args_parser.add_option("-b", "--backup", dest="backup", help="runs the specified backup")
    (options, args) = args_parser.parse_args()

    if options.configfile is not None:
        cfg = options.configfile
    else:
        cfg = "ngrba.conf"
    
    ngrba = NGrba(cfg)
   
    if options.daemon == "start":
        ngrba.start_daemon()
        
    elif options.daemon == "stop":
        ngrba.stop_daemon()
               
    else:
        ngrba.start_backup_thread()
        if options.backup:
            ngrba.run_backup(options.backup.split(","))
        else:
            ngrba.run_backup()
        
        